Repository navigation
feat(approvals,mcp): an approver's agent lists the approvals waiting on them over MCP - #33117
Conversation
…aiReadable list_objects, describe_object, query_records, get_record and aggregate_records ask the bridge's new aiReadExposed attestation (isAiReadExposed over the whole served definition) before refusing a sys_* name; every write tool keeps refusing. Both bridges answer it. sys_approval_request declares enable.aiReadable: true. query_records names the approvals-waiting-on-you filter shape. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
The runtime bridge's aiReadExposed answer and the approvals declaration get their own pins; the end-to-end dogfood pin moves out of this change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
… the read tools Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…ion pin Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…answers waiting-on-me with the service's fold member_default names sys_approval_request (read only); request-read-gate.ts narrows the rows as before. A conjunctive pending_approvers $contains of the caller's own id is answered with the pending-approver index over the caller's acting addresses, so email- and position-addressed slots are found. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…iting on them over MCP Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
… email and position slots Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
📓 Docs Drift CheckThis PR changes 5 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 55 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7a64a05ba29b0af6b99891bf5f52ad189cb9fe24 && git checkout 7a64a05ba29b0af6b99891bf5f52ad189cb9fe24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 128b6571f476d4fae347d8f860b04b95927d2cce b6acbd9a9b1148565de3aa69a6334b020019674a && git checkout -B drift-repro 128b6571f476d4fae347d8f860b04b95927d2cce && git merge --no-ff b6acbd9a9b1148565de3aa69a6334b020019674a
node scripts/docs-audit/affected-docs.mjs --json 128b6571f476d4fae347d8f860b04b95927d2cce
|
|
domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq Contract reviewServed-tier: Face: declaration. ① Derived judgmentsRight
Must-fix
Should-fix
Nit
② Semver level
Prose, which is not this record's face and is named because the brief asked: ③ Boundary flags
Implemented-by: VERDICT: FAIL Must-fix: A (two red lanes), B (the queue pin), C (withheld). Should-fix: D, E. A delta review by the same reviewer is owed on the next head. |
Ruling 6849636735 (1B) gives every member a data-door read on the approval request table, with the participant gate deciding rows. Two suites leaned on members having no read at all: - the action door's denied-load forward: its control now withdraws that one baseline line, so the denied load it pins still happens and the forward still has to authorize on its own; - the request definitions served by the approvals delegate: the request definition now follows the member read to every member, its rows are the participant set (the approver reads their own, a stranger none), and the action log stays delegate-only and closed on the data door. The waiting-on-me rig's aggregate and count options take their own engine option types. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…64-8wgh) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…ember read changed - sys_approval_request declares enable.feeds false: its thread is the timeline and its reply the approval_comment action; - the object's schema-read note, the no-middleware warning, the action door's denied-load note and the approvals docs say what ruling 6849636735 (1B) made true; - the HTTP bridge answers every aiReadExposed ask of one request from one merged read; - the query tool names the approvals filter as an example. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
|
domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq Contract reviewServed-tier: This is a DELTA record on round 1 (6857191533, FAIL at ① Derived judgmentsRound-1 items
Red lines on the delta
Should-fix
Nit
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Must-fix: C (GHSA-2985-5f64-8wgh, withheld). Should-fix: F. Closed from round 1: A, B, C2, D, E. A delta review by the same reviewer is owed on the next head. |
… (GHSA-2985-5f64-8wgh) The writer omits the listed columns from new copies, a rewrite module handles copies at rest (its command is a follow-up), and until then the read seams and the query guards cover them. Revert the request's feeds declaration. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…ainst every object Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
|
domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq Contract reviewServed-tier: This is a DELTA record on round 2 (6859509619, FAIL at ① Derived judgmentsRound-2 items
Red lines on the delta
Should-fix
Nits
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Must-fix: none. Should-fix: G, H, I. Closed from round 2: C (GHSA-2985-5f64-8wgh), C2; F is moot. For the seat before ACCEPT: the two human-floor lines, the claim re-post and the running shard under ③. |
…both copy rewrites in tests The parent-field query guard's header, its refusal remedy and its fail-closed warning now name the case a pinned query is judged against every object. New tests drive migrateSideCopies and migrateArchivedSideCopies end to end: dry run, apply, idempotent re-run, the objects option, and an archive store. The activity redaction's re-export of the column list is dropped; its test imports the list directly. The changeset states who the query rule refuses. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
|
domain:server#5 · os-elon-musk · session_016WV4cwJx2CfBxrD3ojeDPr os-dev report — #32442 (read half) · ready_for_review · PR #33117 · head b6acbd9 · 2026-11-13T20:26ZFinal push after contract review round 3 (PASS, 6862530251): its should-fix G, H and I are done, {
"issue": 32442,
"status": "ready_for_review",
"branch": "claude/project-thread-bb7kzf",
"pr": 33117,
"head": "b6acbd9a9b",
"session": "session_016WV4cwJx2CfBxrD3ojeDPr",
"premise_still_valid": true,
"summary": "Read admission (ruling A), 1B member_default read on sys_approval_request, 2B waiting-on-me answered with the approvals fold, as in rounds 1-3. Round 2: hardening of the activity and audit copies of approval requests (GHSA-2985-5f64-8wgh) in plugin-audit, with an exported at-rest rewrite; enable.feeds:false reverted. This push: G the changeset and PR body state who the copy-column query rule refuses (a reader withheld any field of any object, administrators included, on a request-pinned ledger query naming a copy column) and drop the until-rewrite wording; H the guard's header, remedy and fail-closed warning name that case; I side-copy-migration.test.ts drives migrateSideCopies and migrateArchivedSideCopies; nit: the ACTIVITY_SIDE_COPY_COLUMNS alias dropped. The rewrite's amendment name stays until the owed CLI door settles it.",
"tests": "on b6acbd9a9b: plugin-audit 1495, plugin-approvals 1632, mcp 1121, runtime 10584 (19 skipped), plugin-security 6605 (45 skipped), verify 437, dogfood 52 files naming sys_approval_request/sys_activity/sys_audit_log 390, all passed; typecheck plugin-audit, plugin-approvals (with check:test-typecheck), mcp, runtime, dogfood exit 0; check-test-source-alias exit 0.",
"ablations": "rounds 1-2 as in the PR body (A1, A2, A3, 1B, 2B, W, G, R); this push adds tests only and prose.",
"gates": "dispatch-gates --ran on b6acbd9a9b: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.",
"dogfood_battery": "n/a — no ! in the title and no (narrowing) arm",
"mcp_calls": "0 this round",
"api_writes": "3 this push — claim re-post 6863983815, PR body refresh, this comment",
"contract_review": "round 3 PASS 6862530251 on a12e7c9c9b; delta since is G/H/I prose, tests and a main merge",
"owed": "the os migrate door for the at-rest rewrite: a follow-up card the seat files after landing",
"open_questions": []
} |
|
domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq ACCEPT · 2026-11-13T20:28Z · head
|
domain:server#5 · os-elon-musk · session_016WV4cwJx2CfBxrD3ojeDPr
Requested by Elon · project thread
Fixes #32442
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
enable.aiReadable: true的系统对象(写工具照旧全部拒绝sys_*),sys_approval_request声明了它;② 按你的 1B,成员默认权限集member_default给审批请求加只读,看哪几行仍由审批读闸门决定(提交人、当前审批人、经手人);③ 按你的 2B,读闸门在查询写成「pending_approvers包含我的 id」时,用审批服务自己的地址折算来回答,所以按邮箱、按无人在岗的职位寄出的单子也能找到。控制台的「我的待审」视图用的是同一个过滤,也一起修好。另外:审批请求的活动记录与审计记录副本做了加固,见 GHSA-2985-5f64-8wgh;已有旧记录的批量改写命令列为后续(owed)。加固带来一处收窄:在活动记录或审计日志上,钉住审批请求、又按副本列筛选或全文搜索的查询,现在要求读者能看到所有对象的所有字段,否则拒绝(包括被屏蔽了任一字段的组织管理员);钉住别的对象、或不碰副本列的查询不变。成员现在能在自己参与的审批请求上看和发通用评论,这是 1B 的后果,席位已接受。sys_approval_action不变,仍只经审批委托提供定义、数据侧仍关闭。sys_*对象都不开写;字段屏蔽照旧(审批人拿不到他没权限的快照字段)。回滚即撤本 PR。packages/plugins/plugin-approvals/src/request-read-gate.ts的splitWaitingOnCaller和闸门里的改写;②packages/plugins/plugin-security/src/objects/default-permission-sets.ts新加的一行;③packages/mcp/src/mcp-http-tools.ts的readGuard。pnpm --filter @objectstack/plugin-approvals exec vitest run src/request-read-waiting-on-caller.integration.test.ts,预期 7 条全过:按 id 寄出的审批人只拿到自己那张;按职位寄出的单子,职位持有人用查询、计数、分组计数都拿到;局外人和提交人什么都拿不到。What changes
Read admission (ruling A).
packages/mcp/src/mcp-http-tools.ts.McpDataBridgegains an optional memberaiReadExposed(name): Promise<boolean>. A newreadGuardguards the read tools only (describe_object,query_records,get_record,aggregate_records): the existingguard, except that asys_*name the bridge attests is admitted.list_objectslists asys_*object only where the same attestation holds. Fail-closed: a bridge without the member, or one whose answer throws, keeps every system object refused and unlisted. The write tools,validate_expressionand the file tools keepguard, unchanged.query_records' description gives the "waiting on you" filter onsys_approval_requestas an example.isAiReadExposed(@objectstack/spec/data, spec: a per-object, read-only AI exposure declaration so an agent can read a declared system object through the MCP record tools (#32442 read half) #32865) over the whole served definition:packages/runtime/src/domains/mcp.ts(HTTP) andpackages/mcp/src/stdio-data-bridge.ts(stdio). The stored-metadata family answersfalsebefore the key is read; only the booleantrueopens.sys_approval_requestdeclaresenable: { aiReadable: true }.sys_approval_actiondeclares nothing.Members read their requests (ruling 1B).
packages/plugins/plugin-security/src/objects/default-permission-sets.ts:member_defaultnamessys_approval_requestwithallowRead: trueand every writefalse, aftersys_activity, following the [Decision] Every non-admin member of a new app sees "no permission" for comments and activity on records they can read #30108 precedent (the baseline names a plugin object; the plugin's own gate decides rows). The rows a member reads are stillrequest-read-gate.ts's participant set: the submitter, a current approver, a past actor."Waiting on me" answered with the approvals fold (ruling 2B).
packages/plugins/plugin-approvals/src/request-read-gate.ts:splitWaitingOnCaller(where, userId)takes out a conjunctive (root, or inside a root$and)pending_approvers: { $contains: <the caller's own id> }, exactly that single operator. The gate replaces it withid in <the requests waiting on the caller>, from a newRequestVisibilitySource.waitingOnCallerIds(context), and then narrows to the participant set as for every read. A predicate naming another id, using another operator, or under$oris read as written.packages/plugins/plugin-approvals/src/approval-service.ts:waitingOnCallerIdsanswers withapproverRequestIds(actingAddresses(caller), org), the same fold and pending-approver index the inbox already uses (approver-address.ts; no second fold). The index holds rows only while a request is pending, so a decided request leaves the answer.{current_user_id}before the gate runs, so the console'smy_pendinglist view and an MCPwheretake the same path.@objectstack/plugin-audit: the activity and audit copies of approval requests are hardened (GHSA-2985-5f64-8wgh).side-copy-columns.tslists, by object, the columns no ledger copy carries. The writer (ledgerView) omits them from newsys_activity/sys_audit_logcopies, andside-copy-migration.ts(migrateSideCopies,migrateArchivedSideCopies, on the shared ledger-copy runner, in the key-material rewrite's shape) rewrites the copies at rest. Both ledgers' read redactions drop the listed columns from an older copy, whether or not the rewrite has run. Who can query those copies narrows: a non-system query onsys_activityorsys_audit_logthat namessys_approval_requestby equality onobject_nameand names a column holding the copy (metadata;old_value,new_value), as a free-text search does, is judged against every registered object (reachesSideCopy), as a query naming no object already is. A reader withheld any field of any object, an organization administrator included, is refused it (403) wheremainanswers it. The same query pinned to another object, or naming only other columns, is judged as before; a reader served every field of every object (the stock administrator the existing admin-search pins boot) is answered as before. The guard's header, refusal remedy and fail-closed warning say so. This does not depend on the rewrite having run. Owed: theos migratedoor for that rewrite, as a follow-up card.What 1B supersedes in decision 6840464379. That decision served the two request definitions through
schemaReadDelegate: 'approvals'while the rows stayed closed to members on the data door. After 1B: thesys_approval_requestdefinition reaches every member through the baseline read (projected by their field security), and its rows reach the participants on the data door;sys_approval_actionis unchanged (definition by the delegate, rows closed). The delegate stays on both objects for a deployment that withdraws the baseline line.approval-request-schema-party-read.dogfood.test.tsis re-grounded to exactly that, andaction-door-api-forward-denied-subject.test.tswithdraws the one baseline line so its denied-load forward is still the thing under test.Statements this PR made false, corrected: the schema-read note in
sys-approval-request.object.ts, the no-middleware warning inapprovals-plugin.ts, the denied-load note inpackages/runtime/src/action-execution.ts, andcontent/docs/automation/approvals.mdx(the visibility callout and the record-page paragraph). The spec prose atobject.zod.tsgoes to a follow-up.list_objectscost. The HTTP bridge answers everyaiReadExposedask of one request from one merged read (pinned: three asks, onegetMetaItems).Docs and changeset.
content/docs/ai/connect-mcp.mdxandpackages/mcp/README.mdstate the read opt-in, the member read and the slot coverage;query_records' text gives the approvals filter as an example..changeset/mcp-read-tools-ai-readable-system-object.md:@objectstack/mcp,@objectstack/runtime,@objectstack/plugin-approvals,@objectstack/plugin-security,@objectstack/plugin-auditpatch, with both declaration lines.Tests
packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts(new, 8 tests): realSecurityPlugin, flows, approvals and MCP plugins over the live/mcptransport. No permission set in the file namessys_approval_request: every member holds the shipped baseline and an app set. One flow waits on the approver addressed by email and, once approved, stamps the record; another waits on a team. The approver's "pending for me" returns exactly their two requests with the inbox row's display fields (process and step label, submitter, record); the team member's returns exactly the team's; the submitter's returns nothing, though their untargeted read returns all three; a member on no request reads nothing by query, by id or by aggregate (n: 0); the capability-gated masked field in the snapshot is not served to the approver but is to the submitter who holds the capability;list_objectslistssys_approval_requestas the onlysys_*object; the three write tools are refused andsys_approval_actionstays refused; the tool text names the filter; end to end, the submitter files over MCP, the approver lists, approves withrun_actionand confirmation, the request isapproved, the flow resumes down its approve edge and stamps the record, and the pending list shrinks to the other request.packages/plugins/plugin-approvals/src/request-read-waiting-on-caller.integration.test.ts(new, 7 tests, real ObjectQL over better-sqlite3 and the real pluginstart()): a user-id slot gives the approver exactly that request; a request whose slot is the unstaffedposition:<p>literal is found by its holder via find, count and grouped count; an outsider and the submitter get nothing; a predicate naming another id is left literal and still narrowed; a decided request leaves the answer; puresplitWaitingOnCallercases.plugin-audit:side-copy-columns.test.ts(new, 7: the list,constructorand other inherited names unlisted, both rewrite planners idempotent, the guard's reach, the ledger redaction) andactivity-side-copy-redaction.test.ts(new, 5), andside-copy-migration.test.ts(new, 4:migrateSideCopiesdry run writes nothing; apply rewrites only the listed object's copies in both tables, keeps every other key, column andupdated_at, and a re-run rewrites none; theobjectsoption;migrateArchivedSideCopiesover a real SQL archive store, idempotent).packages/mcp/src/mcp-system-object-ai-read.test.ts(new, 16 tests),packages/runtime/src/mcp-system-object-ai-read.test.ts(new, 7 tests, one merged read for several asks),sys-approval-request.object.test.ts(declaration pins), and two bridge-member tripwires classifying the new member as a no-row-body read.Ablations (
scripts/ablation-replace.mjs, each on a committed head, each restore verifiedblob == HEADandgit diff HEADempty):readGuardadmits nothing (return bad;)falseaiReadable: trueline removedmember_defaultline removed (plugin-security rebuilt;ablation-dist-preflight.mjs --absentproved the grant gone fromdist/, and present again after restore)403 PERMISSION_DENIEDif (false))n: 2and the end-to-end pins red (4 failed, 4 passed)omitSideCopyFromCopycall removed (plugin-audit rebuilt)reachesSideCopycondition removed (plugin-audit rebuilt)activity-side-copy-redaction.test.ts2 failedThe team-addressed pin stays green under the 2B ablation: a team slot is expanded to its members' user ids when the step opens, so it never depended on the fold. It pins the outcome, not 2B.
Acceptance notes
@objectstack/mcpbuilt from4462b4d9: every read ofsys_approval_requestansweredObject "sys_approval_request" is a system object and is not exposed via MCP. After admission alone (report 6848413320):403 PERMISSION_DENIEDfor a member (F1), and an email-addressed approver's "pending for me" returned 0 rows under a test grant (F2). The maintainer ruled 1B + 2B (6849636735); with both, the pins above are green with no test grant.n: 0).list_objectscarries no row counts (itstotalCountcounts objects), so it lists the object to every member and serves no rows. Pinned in the dogfood and the integration test.sys_*object: the write tools keepguard, pinned per tool over MCP; the member baseline line grants read only.mainanswers. It is part of the fix, for the seat's ACCEPT.sys_commentthreads on a request they are party to (the generic comment door needs only the parent read).enable.feedsstays as onmain: declaring itfalsewould refuse administrators' comment writes, whichmainaccepts.mcp-http-tools.ts;mainwas merged once (a596c03c) and merges cleanly now. The 18.4.0 merge freeze has lifted.Verification
Head
b6acbd9a9b:33368c3153(the guard's words, the rewrite tests, the changeset's query rule, after round 3's PASS 6862530251) and one merge ofmain(29 commits, no conflict).Derived gates.
node scripts/pm/dispatch-gates.mjs --ranonb6acbd9a9b: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all 107 exit 0. Before round 2's head two reds were fixed:check:cli-command-idson a command name in the rewrite's report text, and the administrators' ledger-search pins under a first guard design that judged every ledger query reaching a request against every object. The scoped rule keeps those pins green unchanged; it still refuses, for a reader withheld any field of any object, the request-pinned copy-column query described above.Package suites on
b6acbd9a9b, each underos-verify-lock, afterpnpm install --frozen-lockfileand a turbo build of the dogfood, runtime and verify closures:node scripts/check-test-source-alias.mjspnpm --filter <pkg> typecheckfor plugin-audit, plugin-approvals (both withcheck:test-typecheck), mcp, runtime, dogfoodpnpm --filter @objectstack/plugin-audit testpnpm --filter @objectstack/plugin-approvals testpnpm --filter @objectstack/mcp testpnpm --filter @objectstack/runtime testpnpm --filter @objectstack/plugin-security testpnpm --filter @objectstack/verify testpnpm --filter @objectstack/dogfood exec vitest runon the 52 dogfood files that namesys_approval_request,sys_activityorsys_audit_logNot run here. The CI-only families that
dispatch-gateslists outside the derived total: Build Core, the full dogfood shards (the 52 files above stand in for them), the Temporal live-DB jobs, and the type-check lanes.Body refreshed 2026-11-13T20:25Z for the final push after contract review round 3 (6862530251).
🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr